@oneuptime/common 14.0.6 → 14.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/Models/DatabaseModels/InventoryItem.ts +23 -12
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  4. package/Server/Services/AlertService.ts +32 -36
  5. package/Server/Services/IncidentAlertService.ts +609 -2
  6. package/Server/Services/IncidentService.ts +134 -44
  7. package/Server/Services/InventoryItemService.ts +38 -0
  8. package/Server/Services/IoTDeviceCredentialService.ts +13 -4
  9. package/Server/Services/ScheduledMaintenanceService.ts +50 -46
  10. package/Server/Services/UserNotificationRuleService.ts +55 -53
  11. package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
  12. package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
  13. package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
  14. package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
  15. package/Server/Utils/Response.ts +31 -0
  16. package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
  17. package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
  18. package/Server/Utils/Topology/TopologyRequest.ts +463 -0
  19. package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
  20. package/Server/Utils/Topology/TopologySql.ts +1024 -0
  21. package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
  22. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
  23. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
  24. package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
  25. package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
  26. package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
  27. package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
  28. package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
  29. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
  30. package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
  31. package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
  32. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
  33. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
  34. package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
  35. package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
  36. package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
  37. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
  38. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
  39. package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
  40. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
  41. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
  42. package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
  43. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
  44. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
  45. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
  46. package/Tests/Server/Services/DomainService.test.ts +525 -0
  47. package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
  48. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
  49. package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
  50. package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
  51. package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
  52. package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
  53. package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
  54. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
  55. package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
  56. package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
  57. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
  58. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
  59. package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
  60. package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
  61. package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
  62. package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
  63. package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
  64. package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
  65. package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
  66. package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
  67. package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
  68. package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
  69. package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
  70. package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
  71. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
  72. package/Types/Incident/IncidentAlertLink.ts +19 -0
  73. package/Types/Telemetry/InventoryLiveness.ts +26 -0
  74. package/Types/Topology/TopologyApi.ts +428 -0
  75. package/Types/Topology/TopologyTypeRules.ts +191 -0
  76. package/Utils/Incident/IncidentFromAlerts.ts +78 -0
  77. package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
  78. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
  81. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  82. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  83. package/build/dist/Server/Services/AlertService.js +21 -33
  84. package/build/dist/Server/Services/AlertService.js.map +1 -1
  85. package/build/dist/Server/Services/IncidentAlertService.js +457 -3
  86. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
  87. package/build/dist/Server/Services/IncidentService.js +96 -42
  88. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  89. package/build/dist/Server/Services/InventoryItemService.js +31 -0
  90. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  91. package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
  92. package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
  93. package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
  94. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  95. package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
  96. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  97. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
  98. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  99. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
  100. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
  101. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
  102. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
  103. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
  104. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
  105. package/build/dist/Server/Utils/Response.js +30 -0
  106. package/build/dist/Server/Utils/Response.js.map +1 -1
  107. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
  108. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
  109. package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
  110. package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
  111. package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
  112. package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
  113. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
  114. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
  115. package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
  116. package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
  117. package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
  118. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
  119. package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
  120. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
  121. package/build/dist/Types/Topology/TopologyApi.js +55 -0
  122. package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
  123. package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
  124. package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
  125. package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
  126. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
  127. package/package.json +1 -1
@@ -12,6 +12,7 @@ import TableColumn from "../../Types/Database/TableColumn";
12
12
  import TableColumnType from "../../Types/Database/TableColumnType";
13
13
  import TableMetadata from "../../Types/Database/TableMetadata";
14
14
  import TenantColumn from "../../Types/Database/TenantColumn";
15
+ import UNSYNCHRONIZED_INDEX from "../../Types/Database/UnsynchronizedIndex";
15
16
  import IconProp from "../../Types/Icon/IconProp";
16
17
  import { JSONObject } from "../../Types/JSON";
17
18
  import ObjectID from "../../Types/ObjectID";
@@ -20,8 +21,7 @@ import EntitySource from "../../Types/Telemetry/EntitySource";
20
21
  import EntityType from "../../Types/Telemetry/EntityType";
21
22
  import {
22
23
  InventoryLiveness,
23
- INVENTORY_LIVE_WINDOW_MINUTES,
24
- INVENTORY_STALE_AFTER_MINUTES,
24
+ getInventoryLivenessSql,
25
25
  } from "../../Types/Telemetry/InventoryLiveness";
26
26
  import {
27
27
  Column,
@@ -125,6 +125,24 @@ const UPDATE_PERMS: Array<Permission> = [
125
125
  * the tenant column rather than being scanned per row.
126
126
  */
127
127
  @Index(["projectId", "isArchived"])
128
+ /*
129
+ * Lookup by entity key alone. Relationships name their ends by key, never by
130
+ * type, so every "what is at the other end" join (the Topology API's callee,
131
+ * placement and drawer lookups) resolves keys without knowing the type. The
132
+ * unique index above can only serve that as one probe per type in the
133
+ * project; this serves it in one.
134
+ *
135
+ * Created by migration 1795200000000-AddInventoryItemProjectEntityKeyIndex,
136
+ * which carries the timeouts and the CONCURRENTLY runbook for large installs
137
+ * that a generated migration cannot. `synchronize: false` keeps the schema
138
+ * builder from dropping it or generating a second copy (see
139
+ * UnsynchronizedIndex).
140
+ */
141
+ @Index(
142
+ "IDX_INVENTORY_ITEM_PROJECT_ENTITY_KEY",
143
+ ["projectId", "entityKey"],
144
+ UNSYNCHRONIZED_INDEX,
145
+ )
128
146
  export default class InventoryItem extends DatabaseBaseModel {
129
147
  @ColumnAccessControl({ create: CREATE_PERMS, read: READ_PERMS, update: [] })
130
148
  @TableColumn({
@@ -385,9 +403,8 @@ export default class InventoryItem extends DatabaseBaseModel {
385
403
  * status without persisting a value that becomes stale as time passes.
386
404
  * A virtual column needs no schema migration and is never written.
387
405
  *
388
- * The badge floors elapsed minutes before applying inclusive thresholds:
389
- * 30m59s is still Live, and 24h00m59s is still Recent. Strict comparisons
390
- * against the following minute preserve those exact boundaries.
406
+ * The SQL is shared with the Inventory Overview's "Gone Quiet" count — see
407
+ * getInventoryLivenessSql for the boundary rules.
391
408
  */
392
409
  @ColumnAccessControl({ create: [], read: READ_PERMS, update: [] })
393
410
  @TableColumn({
@@ -400,13 +417,7 @@ export default class InventoryItem extends DatabaseBaseModel {
400
417
  @VirtualColumn({
401
418
  type: ColumnType.ShortText,
402
419
  query: (alias: string): string => {
403
- return `CASE
404
- WHEN ${alias}."source" IS DISTINCT FROM '${EntitySource.Discovered}' THEN '${InventoryLiveness.NotTracked}'
405
- WHEN ${alias}."lastSeenAt" IS NULL THEN '${InventoryLiveness.Never}'
406
- WHEN ${alias}."lastSeenAt" > CURRENT_TIMESTAMP - INTERVAL '${INVENTORY_LIVE_WINDOW_MINUTES + 1} minutes' THEN '${InventoryLiveness.Live}'
407
- WHEN ${alias}."lastSeenAt" > CURRENT_TIMESTAMP - INTERVAL '${INVENTORY_STALE_AFTER_MINUTES + 1} minutes' THEN '${InventoryLiveness.Recent}'
408
- ELSE '${InventoryLiveness.Stale}'
409
- END`;
420
+ return getInventoryLivenessSql(alias);
410
421
  },
411
422
  })
412
423
  public inventoryStatus?: InventoryLiveness = undefined;
@@ -0,0 +1,217 @@
1
+ import logger from "../../../Utils/Logger";
2
+ import { MigrationInterface, QueryRunner } from "typeorm";
3
+
4
+ export const INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX: string =
5
+ "IDX_INVENTORY_ITEM_PROJECT_ENTITY_KEY";
6
+
7
+ /*
8
+ * Above this many InventoryItem rows the index is not built here (see
9
+ * "LOCKING" below). Roughly ten seconds of build on modest hardware.
10
+ */
11
+ export const INVENTORY_ITEM_INLINE_INDEX_BUILD_MAX_ROWS: number = 1_000_000;
12
+
13
+ /* What an operator runs to build the index online, under exactly this name. */
14
+ export const INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX_RUNBOOK: string = `CREATE INDEX CONCURRENTLY IF NOT EXISTS "${INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX}" ON "InventoryItem" ("projectId", "entityKey")`;
15
+
16
+ /*
17
+ * Adds ("projectId", "entityKey") on InventoryItem.
18
+ *
19
+ * WHY
20
+ *
21
+ * Relationships name both ends by entity key only. The Topology API resolves
22
+ * those keys to items constantly — the things services call, where they run,
23
+ * the other end of every row in the detail drawer — and the only index that
24
+ * leads with the key today is the unique ("projectId", "entityType",
25
+ * "entityKey"), which a type-less lookup can use only as one probe per type in
26
+ * the project. The queries are written to stay correct and acceptable without
27
+ * this index (they constrain "entityType" to the project's types so the
28
+ * unique index is usable), because on Helm installs migrations run
29
+ * asynchronously and the new API can serve traffic before this has been
30
+ * built. This makes the same lookups one probe.
31
+ *
32
+ * Relationships get no new index on purpose: InventoryItemRelationship is the
33
+ * largest inventory table and a plain CREATE INDEX would block its ingest
34
+ * writes for the whole build.
35
+ *
36
+ * WHY THIS IS HAND-WRITTEN
37
+ *
38
+ * A generated migration cannot bound its own lock wait or runtime, nor decide
39
+ * to leave a build to the operator, and this one runs against a table that
40
+ * ingest writes continuously. The entity declares the index by name with
41
+ * `synchronize: false` (see Types/Database/UnsynchronizedIndex.ts), so the
42
+ * schema builder neither generates a second copy nor drops this one.
43
+ *
44
+ * LOCKING, AND THE RUNBOOK FOR LARGE INSTALLS
45
+ *
46
+ * Migrations run one transaction each (migrationsTransactionMode "each"), so
47
+ * CREATE INDEX CONCURRENTLY is not available here. A plain CREATE INDEX holds
48
+ * a SHARE lock on InventoryItem for as long as the build takes: reads
49
+ * continue, inventory writes queue behind it and fail once their own
50
+ * lock_timeout (3 s in the app) or statement_timeout runs out.
51
+ *
52
+ * The build cannot be given more time than the connection allows. The
53
+ * effective ceiling is the client-side DATABASE_QUERY_TIMEOUT_MS of
54
+ * node-postgres (35 s by default), which no SET can raise: past it the client
55
+ * abandons the query — and the ROLLBACK queued behind it — while the server
56
+ * keeps building and keeps the lock. So statement_timeout stays at the
57
+ * connection's own value (30 s by default, below that ceiling; `DEFAULT`
58
+ * undoes anything an earlier statement on the session may have set), which
59
+ * makes Postgres cancel an overlong build itself and release the lock at
60
+ * once. `lock_timeout` stops the migration from queueing behind a long
61
+ * transaction (and parking every writer behind itself) for more than five
62
+ * seconds; a timeout fails the migration, which the next start retries.
63
+ *
64
+ * A build that would not finish comfortably inside that window is not
65
+ * attempted: when InventoryItem holds more than
66
+ * INVENTORY_ITEM_INLINE_INDEX_BUILD_MAX_ROWS rows (the planner's estimate
67
+ * from pg_class; a bounded count when the table has never been analyzed),
68
+ * this migration logs a warning and completes without the index. The
69
+ * Topology API is correct without it, only slower on key lookups. Such an
70
+ * install should build the index online, at any time, under exactly this
71
+ * name:
72
+ *
73
+ * CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_INVENTORY_ITEM_PROJECT_ENTITY_KEY"
74
+ * ON "InventoryItem" ("projectId", "entityKey");
75
+ *
76
+ * Built before the upgrade, this migration finds it and does nothing. A
77
+ * CONCURRENTLY build that failed leaves an INVALID index behind under that
78
+ * name, which IF NOT EXISTS would otherwise accept forever; such a leftover
79
+ * is dropped here first (so the runbook works as written afterwards) and
80
+ * rebuilt when the table is small enough.
81
+ *
82
+ * An online build that is still RUNNING looks exactly like that leftover
83
+ * (pg_index.indisvalid is false until it finishes). Dropping it would queue
84
+ * for an ACCESS EXCLUSIVE lock behind the build and stall every reader and
85
+ * writer of InventoryItem meanwhile, so a build in progress
86
+ * (pg_stat_progress_create_index) is left to finish and this migration
87
+ * completes without touching it.
88
+ */
89
+ export class AddInventoryItemProjectEntityKeyIndex1795200000000
90
+ implements MigrationInterface
91
+ {
92
+ public name: string = "AddInventoryItemProjectEntityKeyIndex1795200000000";
93
+
94
+ /* Lowered only by tests; TypeORM constructs migrations without arguments. */
95
+ public constructor(
96
+ private maxRowsForInlineBuild: number = INVENTORY_ITEM_INLINE_INDEX_BUILD_MAX_ROWS,
97
+ ) {}
98
+
99
+ public async up(queryRunner: QueryRunner): Promise<void> {
100
+ await queryRunner.query(`SET LOCAL statement_timeout = DEFAULT`);
101
+ await queryRunner.query(`SET LOCAL lock_timeout = '5s'`);
102
+
103
+ const existing: Array<{ isValid: boolean }> = await queryRunner.query(
104
+ `SELECT x.indisvalid AS "isValid" FROM pg_index x
105
+ JOIN pg_class c ON c.oid = x.indexrelid
106
+ JOIN pg_namespace n ON n.oid = c.relnamespace
107
+ WHERE c.relname = $1 AND n.nspname = current_schema()`,
108
+ [INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX],
109
+ );
110
+
111
+ if (
112
+ existing.some((index: { isValid: boolean }): boolean => {
113
+ return index.isValid === true;
114
+ })
115
+ ) {
116
+ /* Pre-built by an operator (or already migrated): nothing to do. */
117
+ return;
118
+ }
119
+
120
+ if (existing.length > 0) {
121
+ if (await this.isBuildInProgress(queryRunner)) {
122
+ logger.warn(
123
+ `AddInventoryItemProjectEntityKeyIndex: an online build of ${INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX} is in progress; leaving it to finish. If that build fails, drop the INVALID index it leaves behind and run again: ${INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX_RUNBOOK};`,
124
+ );
125
+ return;
126
+ }
127
+ await queryRunner.query(
128
+ `DROP INDEX IF EXISTS "${INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX}"`,
129
+ );
130
+ }
131
+
132
+ const rows: number = await this.estimateRows(queryRunner);
133
+
134
+ if (rows > this.maxRowsForInlineBuild) {
135
+ logger.warn(
136
+ `AddInventoryItemProjectEntityKeyIndex: InventoryItem holds about ${Math.round(
137
+ rows,
138
+ )} rows, more than the ${this.maxRowsForInlineBuild} this migration builds an index over while it blocks inventory writes, so the index ${INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX} was NOT created. The Topology maps work without it but resolve entity keys more slowly. Build it online (it does not block writes) by running on the OneUptime database: ${INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX_RUNBOOK};`,
139
+ );
140
+ return;
141
+ }
142
+
143
+ await queryRunner.query(
144
+ `CREATE INDEX IF NOT EXISTS "${INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX}" ON "InventoryItem" ("projectId", "entityKey")`,
145
+ );
146
+ }
147
+
148
+ public async down(queryRunner: QueryRunner): Promise<void> {
149
+ /*
150
+ * DROP INDEX takes ACCESS EXCLUSIVE on the table; waiting for it would
151
+ * park every reader and writer of InventoryItem behind this statement.
152
+ */
153
+ await queryRunner.query(`SET LOCAL statement_timeout = DEFAULT`);
154
+ await queryRunner.query(`SET LOCAL lock_timeout = '5s'`);
155
+ await queryRunner.query(
156
+ `DROP INDEX IF EXISTS "${INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX}"`,
157
+ );
158
+ }
159
+
160
+ /* Whether a CREATE INDEX (CONCURRENTLY) of this index is running now. */
161
+ private async isBuildInProgress(queryRunner: QueryRunner): Promise<boolean> {
162
+ const builds: Array<unknown> = await queryRunner.query(
163
+ `SELECT 1 FROM pg_stat_progress_create_index p
164
+ JOIN pg_class c ON c.oid = p.index_relid
165
+ JOIN pg_namespace n ON n.oid = c.relnamespace
166
+ WHERE c.relname = $1 AND n.nspname = current_schema()`,
167
+ [INVENTORY_ITEM_PROJECT_ENTITY_KEY_INDEX],
168
+ );
169
+ return builds.length > 0;
170
+ }
171
+
172
+ /*
173
+ * How many rows InventoryItem holds, from the statistics the planner uses:
174
+ * pg_class.reltuples scaled from the pages it was measured over to the
175
+ * table's current size (so growth since the last ANALYZE counts). A table
176
+ * that has never been analyzed (reltuples -1), or whose statistics saw no
177
+ * pages although it has some now, is counted instead — only up to just past
178
+ * the threshold, so the count stays cheap on any table.
179
+ */
180
+ private async estimateRows(queryRunner: QueryRunner): Promise<number> {
181
+ const statistics: Array<{
182
+ reltuples: number | string | null;
183
+ relpages: number | string | null;
184
+ pages: number | string | null;
185
+ }> = await queryRunner.query(
186
+ `SELECT c.reltuples::float8 AS "reltuples", c.relpages::float8 AS "relpages",
187
+ (pg_relation_size(c.oid) / current_setting('block_size')::int)::float8 AS "pages"
188
+ FROM pg_class c
189
+ JOIN pg_namespace n ON n.oid = c.relnamespace
190
+ WHERE c.relname = 'InventoryItem' AND n.nspname = current_schema()`,
191
+ );
192
+
193
+ const reltuples: number = Number(statistics[0]?.reltuples ?? -1);
194
+ const relpages: number = Number(statistics[0]?.relpages ?? 0);
195
+ const pages: number = Number(statistics[0]?.pages ?? 0);
196
+
197
+ const isKnown: boolean =
198
+ Number.isFinite(reltuples) &&
199
+ reltuples >= 0 &&
200
+ Number.isFinite(relpages) &&
201
+ Number.isFinite(pages) &&
202
+ (relpages > 0 || pages === 0);
203
+
204
+ if (isKnown) {
205
+ return relpages > 0
206
+ ? Math.max(reltuples, (reltuples / relpages) * pages)
207
+ : reltuples;
208
+ }
209
+
210
+ const counted: Array<{ count: number | string }> = await queryRunner.query(
211
+ `SELECT COUNT(*)::int AS "count" FROM (SELECT 1 FROM "InventoryItem" LIMIT $1) bounded`,
212
+ [this.maxRowsForInlineBuild + 1],
213
+ );
214
+
215
+ return Number(counted[0]?.count ?? 0);
216
+ }
217
+ }
@@ -12,6 +12,7 @@ import { AddSessionReplaySameOriginTracePropagation1794800000000 } from "./17948
12
12
  import { AddIncidentAlert1794900000000 } from "./1794900000000-AddIncidentAlert";
13
13
  import { AddDatabaseServerTables1795000000000 } from "./1795000000000-AddDatabaseServerTables";
14
14
  import { AddUserProjectSsoConsent1795100000000 } from "./1795100000000-AddUserProjectSsoConsent";
15
+ import { AddInventoryItemProjectEntityKeyIndex1795200000000 } from "./1795200000000-AddInventoryItemProjectEntityKeyIndex";
15
16
  import { AddConfigurableRuleCriteria1792500000000 } from "./1792500000000-AddConfigurableRuleCriteria";
16
17
  import { AddGoogleSecOpsDiagnostics1792300000000 } from "./1792300000000-AddGoogleSecOpsDiagnostics";
17
18
  import { AddIncomingCallPolicyPhoneNumbers1792600000000 } from "./1792600000000-AddIncomingCallPolicyPhoneNumbers";
@@ -1224,4 +1225,5 @@ export default [
1224
1225
  AddIncidentAlert1794900000000,
1225
1226
  AddDatabaseServerTables1795000000000,
1226
1227
  AddUserProjectSsoConsent1795100000000,
1228
+ AddInventoryItemProjectEntityKeyIndex1795200000000,
1227
1229
  ];
@@ -20,7 +20,9 @@ import OnCallDutyPolicyService from "./OnCallDutyPolicyService";
20
20
  import TeamMemberService from "./TeamMemberService";
21
21
  import UserService from "./UserService";
22
22
  import URL from "../../Types/API/URL";
23
- import { getSloAffectedResourceMarkdownLines } from "../../Utils/Slo/SloAffectedResourceMarkdown";
23
+ import LinkedAffectedResources, {
24
+ LinkedAffectedResource,
25
+ } from "../Utils/AffectedResources/LinkedAffectedResources";
24
26
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
25
27
  import SortOrder from "../../Types/BaseDatabase/SortOrder";
26
28
  import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
@@ -73,7 +75,6 @@ import AlertReminderRule from "../../Models/DatabaseModels/AlertReminderRule";
73
75
  import WorkspaceType from "../../Types/Workspace/WorkspaceType";
74
76
  import NotificationRuleWorkspaceChannel from "../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
75
77
  import AlertWorkspaceMessages from "../Utils/Workspace/WorkspaceMessages/Alert";
76
- import Monitor from "../../Models/DatabaseModels/Monitor";
77
78
  import ServiceLevelObjective from "../../Models/DatabaseModels/ServiceLevelObjective";
78
79
  import MonitorService from "./MonitorService";
79
80
  import { MessageBlocksByWorkspaceType } from "./WorkspaceNotificationRuleService";
@@ -1024,19 +1025,6 @@ export class Service extends DatabaseService<Model> {
1024
1025
  labels: {
1025
1026
  name: true,
1026
1027
  },
1027
- monitor: {
1028
- name: true,
1029
- _id: true,
1030
- },
1031
- /*
1032
- * This read runs as root, so projectId comes along and the feed
1033
- * names only this project's SLOs (getSloAffectedResourceMarkdownLines).
1034
- */
1035
- serviceLevelObjectives: {
1036
- name: true,
1037
- _id: true,
1038
- projectId: true,
1039
- },
1040
1028
  },
1041
1029
  props: {
1042
1030
  isRoot: true,
@@ -1067,31 +1055,28 @@ ${alert.description || "No description provided."}
1067
1055
  }
1068
1056
 
1069
1057
  /*
1070
- * The monitor, then the SLOs this alert is linked to. A burn-rate alert
1071
- * has no monitor, so its SLO is the only resource there is to name -
1072
- * and the feed's only way back to the objective that raised it. The SLO
1073
- * link is built inline: ServiceLevelObjectiveService cannot be imported
1074
- * here (it reaches this service through the burn-rate rule service).
1058
+ * Everything the alert's Affected Resources card lists: its monitor,
1059
+ * the hosts, clusters and services it is attached to, then its SLOs. A
1060
+ * burn-rate alert has no monitor, so its SLO is the only resource there
1061
+ * is to name - and the feed's only way back to the objective that
1062
+ * raised it.
1075
1063
  */
1076
- const sloLines: Array<string> =
1077
- alert.serviceLevelObjectives && alert.serviceLevelObjectives.length > 0
1078
- ? getSloAffectedResourceMarkdownLines({
1079
- dashboardUrl: await DatabaseConfig.getDashboardUrl(),
1080
- projectId: alert.projectId!,
1081
- serviceLevelObjectives: alert.serviceLevelObjectives,
1082
- })
1083
- : [];
1064
+ const resources: Array<LinkedAffectedResource> =
1065
+ await LinkedAffectedResources.readForAlert({
1066
+ service: this,
1067
+ projectId: alert.projectId!,
1068
+ alertId: alert.id!,
1069
+ });
1084
1070
 
1085
- if (alert.monitor || sloLines.length > 0) {
1071
+ if (resources.length > 0) {
1086
1072
  feedInfoInMarkdown += `🌎 **Resources Affected**:\n`;
1087
1073
 
1088
- if (alert.monitor) {
1089
- const monitor: Monitor = alert.monitor;
1090
- feedInfoInMarkdown += `- [${monitor.name}](${(await MonitorService.getMonitorLinkInDashboard(alert.projectId!, monitor.id!)).toString()})\n`;
1091
- }
1092
-
1093
- for (const sloLine of sloLines) {
1094
- feedInfoInMarkdown += `${sloLine}\n`;
1074
+ for (const resourceLine of LinkedAffectedResources.getMarkdownLines({
1075
+ dashboardUrl: await DatabaseConfig.getDashboardUrl(),
1076
+ projectId: alert.projectId!,
1077
+ resources: resources,
1078
+ })) {
1079
+ feedInfoInMarkdown += `${resourceLine}\n`;
1095
1080
  }
1096
1081
 
1097
1082
  feedInfoInMarkdown += `\n\n`;
@@ -1768,6 +1753,12 @@ ${alertSeverity.name}
1768
1753
  notifyOwners: boolean;
1769
1754
  rootCause: string | undefined;
1770
1755
  stateChangeLog: JSONObject | undefined;
1756
+ /*
1757
+ * The user the change is credited to (the alert's feed names them), for
1758
+ * a change a user asked for that is written on their behalf. Unset for
1759
+ * changes the system makes on its own.
1760
+ */
1761
+ createdByUserId?: ObjectID | undefined;
1771
1762
  props: DatabaseCommonInteractionProps | undefined;
1772
1763
  }): Promise<void> {
1773
1764
  const {
@@ -1777,6 +1768,7 @@ ${alertSeverity.name}
1777
1768
  notifyOwners,
1778
1769
  rootCause,
1779
1770
  stateChangeLog,
1771
+ createdByUserId,
1780
1772
  props,
1781
1773
  } = data;
1782
1774
 
@@ -1822,6 +1814,10 @@ ${alertSeverity.name}
1822
1814
  statusTimeline.rootCause = rootCause;
1823
1815
  }
1824
1816
 
1817
+ if (createdByUserId) {
1818
+ statusTimeline.createdByUserId = createdByUserId;
1819
+ }
1820
+
1825
1821
  await AlertStateTimelineService.create({
1826
1822
  data: statusTimeline,
1827
1823
  props: props || {},